/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */#include"nsAutoPtr.h"#include"sqlite3.h"#include"mozIStorageStatementCallback.h"#include"mozStorageBindingParams.h"#include"mozStorageHelper.h"#include"mozStorageResultSet.h"#include"mozStorageRow.h"#include"mozStorageConnection.h"#include"mozStorageError.h"#include"mozStoragePrivateHelpers.h"#include"mozStorageStatementData.h"#include"mozStorageAsyncStatementExecution.h"#include"mozilla/DebugOnly.h"#include"mozilla/Telemetry.h"namespacemozilla{namespacestorage{/** * The following constants help batch rows into result sets. * MAX_MILLISECONDS_BETWEEN_RESULTS was chosen because any user-based task that * takes less than 200 milliseconds is considered to feel instantaneous to end * users. MAX_ROWS_PER_RESULT was arbitrarily chosen to reduce the number of * dispatches to calling thread, while also providing reasonably-sized sets of * data for consumers. Both of these constants are used because we assume that * consumers are trying to avoid blocking their execution thread for long * periods of time, and dispatching many small events to the calling thread will * end up blocking it. */#define MAX_MILLISECONDS_BETWEEN_RESULTS 75#define MAX_ROWS_PER_RESULT 15//////////////////////////////////////////////////////////////////////////////////// AsyncExecuteStatements/* static */nsresultAsyncExecuteStatements::execute(StatementDataArray&aStatements,Connection*aConnection,sqlite3*aNativeConnection,mozIStorageStatementCallback*aCallback,mozIStoragePendingStatement**_stmt){// Create our event to run in the backgroundRefPtr<AsyncExecuteStatements>event=newAsyncExecuteStatements(aStatements,aConnection,aNativeConnection,aCallback);NS_ENSURE_TRUE(event,NS_ERROR_OUT_OF_MEMORY);// Dispatch it to the backgroundnsIEventTarget*target=aConnection->getAsyncExecutionTarget();// If we don't have a valid target, this is a bug somewhere else. In the past,// this assert found cases where a Run method would schedule a new statement// without checking if asyncClose had been called. The caller must prevent// that from happening or, if the work is not critical, just avoid creating// the new statement during shutdown. See bug 718449 for an example.MOZ_ASSERT(target);if(!target){returnNS_ERROR_NOT_AVAILABLE;}nsresultrv=target->Dispatch(event,NS_DISPATCH_NORMAL);NS_ENSURE_SUCCESS(rv,rv);// Return it as the pending statement object and track it.event.forget(_stmt);returnNS_OK;}AsyncExecuteStatements::AsyncExecuteStatements(StatementDataArray&aStatements,Connection*aConnection,sqlite3*aNativeConnection,mozIStorageStatementCallback*aCallback):mConnection(aConnection),mNativeConnection(aNativeConnection),mHasTransaction(false),mCallback(aCallback),mCallingThread(::do_GetCurrentThread()),mMaxWait(TimeDuration::FromMilliseconds(MAX_MILLISECONDS_BETWEEN_RESULTS)),mIntervalStart(TimeStamp::Now()),mState(PENDING),mCancelRequested(false),mMutex(aConnection->sharedAsyncExecutionMutex),mDBMutex(aConnection->sharedDBMutex),mRequestStartDate(TimeStamp::Now()){(void)mStatements.SwapElements(aStatements);NS_ASSERTION(mStatements.Length(),"We weren't given any statements!");}AsyncExecuteStatements::~AsyncExecuteStatements(){MOZ_ASSERT(!mCallback,"Never called the Completion callback!");MOZ_ASSERT(!mHasTransaction,"There should be no transaction at this point");if(mCallback){NS_ProxyRelease("AsyncExecuteStatements::mCallback",mCallingThread,mCallback.forget());}}boolAsyncExecuteStatements::shouldNotify(){#ifdef DEBUGmMutex.AssertNotCurrentThreadOwns();boolonCallingThread=false;(void)mCallingThread->IsOnCurrentThread(&onCallingThread);NS_ASSERTION(onCallingThread,"runEvent not running on the calling thread!");#endif// We do not need to acquire mMutex here because it can only ever be written// to on the calling thread, and the only thread that can call us is the// calling thread, so we know that our access is serialized.return!mCancelRequested;}boolAsyncExecuteStatements::bindExecuteAndProcessStatement(StatementData&aData,boolaLastStatement){mMutex.AssertNotCurrentThreadOwns();sqlite3_stmt*aStatement=nullptr;// This cannot fail; we are only called if it's available.(void)aData.getSqliteStatement(&aStatement);NS_ASSERTION(aStatement,"You broke the code; do not call here like that!");BindingParamsArray*paramsArray(aData);// Iterate through all of our parameters, bind them, and execute.boolcontinueProcessing=true;BindingParamsArray::iteratoritr=paramsArray->begin();BindingParamsArray::iteratorend=paramsArray->end();while(itr!=end&&continueProcessing){// Bind the data to our statement.nsCOMPtr<IStorageBindingParamsInternal>bindingInternal=do_QueryInterface(*itr);nsCOMPtr<mozIStorageError>error=bindingInternal->bind(aStatement);if(error){// Set our error state.mState=ERROR;// And notify.(void)notifyError(error);returnfalse;}// Advance our iterator, execute, and then process the statement.itr++;boollastStatement=aLastStatement&&itr==end;continueProcessing=executeAndProcessStatement(aStatement,lastStatement);// Always reset our statement.(void)::sqlite3_reset(aStatement);}returncontinueProcessing;}boolAsyncExecuteStatements::executeAndProcessStatement(sqlite3_stmt*aStatement,boolaLastStatement){mMutex.AssertNotCurrentThreadOwns();// Execute our statementboolhasResults;do{hasResults=executeStatement(aStatement);// If we had an error, bail.if(mState==ERROR)returnfalse;// If we have been canceled, there is no point in going on...{MutexAutoLocklockedScope(mMutex);if(mCancelRequested){mState=CANCELED;returnfalse;}}// Build our result set and notify if we got anything back and have a// callback to notify.if(mCallback&&hasResults&&NS_FAILED(buildAndNotifyResults(aStatement))){// We had an error notifying, so we notify on error and stop processing.mState=ERROR;// Notify, and stop processing statements.(void)notifyError(mozIStorageError::ERROR,"An error occurred while notifying about results");returnfalse;}}while(hasResults);#ifndef MOZ_STORAGE_SORTWARNING_SQL_DUMPif(MOZ_LOG_TEST(gStorageLog,LogLevel::Warning))#endif{// Check to make sure that this statement was smart about what it did.checkAndLogStatementPerformance(aStatement);}// If we are done, we need to set our state accordingly while we still hold// our mutex. We would have already returned if we were canceled or had// an error at this point.if(aLastStatement)mState=COMPLETED;returntrue;}boolAsyncExecuteStatements::executeStatement(sqlite3_stmt*aStatement){mMutex.AssertNotCurrentThreadOwns();Telemetry::AutoTimer<Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_MS>finallySendExecutionDuration(mRequestStartDate);while(true){// lock the sqlite mutex so sqlite3_errmsg cannot changeSQLiteMutexAutoLocklockedScope(mDBMutex);intrc=mConnection->stepStatement(mNativeConnection,aStatement);// Stop if we have no more results.if(rc==SQLITE_DONE){Telemetry::Accumulate(Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS,true);returnfalse;}// If we got results, we can return now.if(rc==SQLITE_ROW){Telemetry::Accumulate(Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS,true);returntrue;}// Some errors are not fatal, and we can handle them and continue.if(rc==SQLITE_BUSY){// Don't hold the lock while we call outside our module.SQLiteMutexAutoUnlockunlockedScope(mDBMutex);// Yield, and try again(void)::PR_Sleep(PR_INTERVAL_NO_WAIT);continue;}// Set an error state.mState=ERROR;Telemetry::Accumulate(Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS,false);// Construct the error message before giving up the mutex (which we cannot// hold during the call to notifyError).nsCOMPtr<mozIStorageError>errorObj(newError(rc,::sqlite3_errmsg(mNativeConnection)));// We cannot hold the DB mutex while calling notifyError.SQLiteMutexAutoUnlockunlockedScope(mDBMutex);(void)notifyError(errorObj);// Finally, indicate that we should stop processing.returnfalse;}}nsresultAsyncExecuteStatements::buildAndNotifyResults(sqlite3_stmt*aStatement){NS_ASSERTION(mCallback,"Trying to dispatch results without a callback!");mMutex.AssertNotCurrentThreadOwns();// Build result object if we need it.if(!mResultSet)mResultSet=newResultSet();NS_ENSURE_TRUE(mResultSet,NS_ERROR_OUT_OF_MEMORY);RefPtr<Row>row(newRow());NS_ENSURE_TRUE(row,NS_ERROR_OUT_OF_MEMORY);nsresultrv=row->initialize(aStatement);NS_ENSURE_SUCCESS(rv,rv);rv=mResultSet->add(row);NS_ENSURE_SUCCESS(rv,rv);// If we have hit our maximum number of allowed results, or if we have hit// the maximum amount of time we want to wait for results, notify the// calling thread about it.TimeStampnow=TimeStamp::Now();TimeDurationdelta=now-mIntervalStart;if(mResultSet->rows()>=MAX_ROWS_PER_RESULT||delta>mMaxWait){// Notify the callerrv=notifyResults();if(NS_FAILED(rv))returnNS_OK;// we'll try again with the next result// Reset our start timemIntervalStart=now;}returnNS_OK;}nsresultAsyncExecuteStatements::notifyComplete(){mMutex.AssertNotCurrentThreadOwns();NS_ASSERTION(mState!=PENDING,"Still in a pending state when calling Complete!");// Reset our statements before we try to commit or rollback. If we are// canceling and have statements that think they have pending work, the// rollback will fail.for(uint32_ti=0;i<mStatements.Length();i++)mStatements[i].reset();// Release references to the statement data as soon as possible. If this// is the last reference, statements will be finalized immediately on the// async thread, hence avoiding several bounces between threads and possible// race conditions with AsyncClose().mStatements.Clear();// Handle our transaction, if we have oneif(mHasTransaction){if(mState==COMPLETED){nsresultrv=mConnection->commitTransactionInternal(mNativeConnection);if(NS_FAILED(rv)){mState=ERROR;(void)notifyError(mozIStorageError::ERROR,"Transaction failed to commit");}}else{DebugOnly<nsresult>rv=mConnection->rollbackTransactionInternal(mNativeConnection);NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),"Transaction failed to rollback");}mHasTransaction=false;}// This will take ownership of mCallback and make sure its destruction will// happen on the owner thread.Unused<<mCallingThread->Dispatch(NewRunnableMethod("AsyncExecuteStatements::notifyCompleteOnCallingThread",this,&AsyncExecuteStatements::notifyCompleteOnCallingThread),NS_DISPATCH_NORMAL);returnNS_OK;}nsresultAsyncExecuteStatements::notifyCompleteOnCallingThread(){MOZ_ASSERT(mCallingThread->IsOnCurrentThread());// Take ownership of mCallback and responsibility for freeing it when we// release it. Any notifyResultsOnCallingThread and notifyErrorOnCallingThread// calls on the stack spinning the event loop have guaranteed their safety by// creating their own strong reference before invoking the callback.nsCOMPtr<mozIStorageStatementCallback>callback=mCallback.forget();if(callback){Unused<<callback->HandleCompletion(mState);}returnNS_OK;}nsresultAsyncExecuteStatements::notifyError(int32_taErrorCode,constchar*aMessage){mMutex.AssertNotCurrentThreadOwns();mDBMutex.assertNotCurrentThreadOwns();if(!mCallback)returnNS_OK;nsCOMPtr<mozIStorageError>errorObj(newError(aErrorCode,aMessage));NS_ENSURE_TRUE(errorObj,NS_ERROR_OUT_OF_MEMORY);returnnotifyError(errorObj);}nsresultAsyncExecuteStatements::notifyError(mozIStorageError*aError){mMutex.AssertNotCurrentThreadOwns();mDBMutex.assertNotCurrentThreadOwns();if(!mCallback)returnNS_OK;Unused<<mCallingThread->Dispatch(NewRunnableMethod<nsCOMPtr<mozIStorageError>>("AsyncExecuteStatements::notifyErrorOnCallingThread",this,&AsyncExecuteStatements::notifyErrorOnCallingThread,aError),NS_DISPATCH_NORMAL);returnNS_OK;}nsresultAsyncExecuteStatements::notifyErrorOnCallingThread(mozIStorageError*aError){MOZ_ASSERT(mCallingThread->IsOnCurrentThread());// Acquire our own strong reference so that if the callback spins a nested// event loop and notifyCompleteOnCallingThread is executed, forgetting// mCallback, we still have a valid/strong reference that won't be freed until// we exit.nsCOMPtr<mozIStorageStatementCallback>callback=mCallback;if(shouldNotify()&&callback){Unused<<callback->HandleError(aError);}returnNS_OK;}nsresultAsyncExecuteStatements::notifyResults(){mMutex.AssertNotCurrentThreadOwns();MOZ_ASSERT(mCallback,"notifyResults called without a callback!");// This takes ownership of mResultSet, a new one will be generated in// buildAndNotifyResults() when further results will arrive.Unused<<mCallingThread->Dispatch(NewRunnableMethod<RefPtr<ResultSet>>("AsyncExecuteStatements::notifyResultsOnCallingThread",this,&AsyncExecuteStatements::notifyResultsOnCallingThread,mResultSet.forget()),NS_DISPATCH_NORMAL);returnNS_OK;}nsresultAsyncExecuteStatements::notifyResultsOnCallingThread(ResultSet*aResultSet){MOZ_ASSERT(mCallingThread->IsOnCurrentThread());// Acquire our own strong reference so that if the callback spins a nested// event loop and notifyCompleteOnCallingThread is executed, forgetting// mCallback, we still have a valid/strong reference that won't be freed until// we exit.nsCOMPtr<mozIStorageStatementCallback>callback=mCallback;if(shouldNotify()&&callback){Unused<<callback->HandleResult(aResultSet);}returnNS_OK;}NS_IMPL_ISUPPORTS(AsyncExecuteStatements,nsIRunnable,mozIStoragePendingStatement)boolAsyncExecuteStatements::statementsNeedTransaction(){// If there is more than one write statement, run in a transaction.// Additionally, if we have only one statement but it needs a transaction, due// to multiple BindingParams, we will wrap it in one.for(uint32_ti=0,transactionsCount=0;i<mStatements.Length();++i){transactionsCount+=mStatements[i].needsTransaction();if(transactionsCount>1){returntrue;}}returnfalse;}//////////////////////////////////////////////////////////////////////////////////// mozIStoragePendingStatementNS_IMETHODIMPAsyncExecuteStatements::Cancel(){#ifdef DEBUGboolonCallingThread=false;(void)mCallingThread->IsOnCurrentThread(&onCallingThread);NS_ASSERTION(onCallingThread,"Not canceling from the calling thread!");#endif// If we have already canceled, we have an error, but always indicate that// we are trying to cancel.NS_ENSURE_FALSE(mCancelRequested,NS_ERROR_UNEXPECTED);{MutexAutoLocklockedScope(mMutex);// We need to indicate that we want to try and cancel now.mCancelRequested=true;}returnNS_OK;}//////////////////////////////////////////////////////////////////////////////////// nsIRunnableNS_IMETHODIMPAsyncExecuteStatements::Run(){MOZ_ASSERT(mConnection->isConnectionReadyOnThisThread());// Do not run if we have been canceled.{MutexAutoLocklockedScope(mMutex);if(mCancelRequested)mState=CANCELED;}if(mState==CANCELED)returnnotifyComplete();if(statementsNeedTransaction()&&mConnection->getAutocommit()){if(NS_SUCCEEDED(mConnection->beginTransactionInternal(mNativeConnection,mozIStorageConnection::TRANSACTION_IMMEDIATE))){mHasTransaction=true;}#ifdef DEBUGelse{NS_WARNING("Unable to create a transaction for async execution.");}#endif}// Execute each statement, giving the callback results if it returns any.for(uint32_ti=0;i<mStatements.Length();i++){boolfinished=(i==(mStatements.Length()-1));sqlite3_stmt*stmt;{// lock the sqlite mutex so sqlite3_errmsg cannot changeSQLiteMutexAutoLocklockedScope(mDBMutex);intrc=mStatements[i].getSqliteStatement(&stmt);if(rc!=SQLITE_OK){// Set our error state.mState=ERROR;// Build the error object; can't call notifyError with the lock heldnsCOMPtr<mozIStorageError>errorObj(newError(rc,::sqlite3_errmsg(mNativeConnection)));{// We cannot hold the DB mutex and call notifyError.SQLiteMutexAutoUnlockunlockedScope(mDBMutex);(void)notifyError(errorObj);}break;}}// If we have parameters to bind, bind them, execute, and process.if(mStatements[i].hasParametersToBeBound()){if(!bindExecuteAndProcessStatement(mStatements[i],finished))break;}// Otherwise, just execute and process the statement.elseif(!executeAndProcessStatement(stmt,finished)){break;}}// If we still have results that we haven't notified about, take care of// them now.if(mResultSet)(void)notifyResults();// Notify about completionreturnnotifyComplete();}}// namespace storage}// namespace mozilla